-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support Smithy default
trait
#857
Conversation
val targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) { | ||
var targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT)) { | ||
toSymbol(targetShape).toBuilder().boxed().build() | ||
} else { | ||
toSymbol(targetShape) | ||
} | ||
|
||
targetSymbol = shape.getTrait<DefaultTrait>()?.let { | ||
val builder = targetSymbol.toBuilder() | ||
val defaultValue = it.getDefaultValue(targetShape) | ||
builder.defaultValue(defaultValue) | ||
if (defaultValue != "null") { | ||
builder.unboxed() | ||
} | ||
builder.build() | ||
} ?: targetSymbol |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The var
reference here is unnecessary. You're already dealing with builders is several places so I suggest just keeping it as a builder until you've applied all the customizations necessary:
val targetSymbol = toSymbol(targetShape)
.toBuilder()
.apply {
if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT)) boxed()
shape.getTrait<DefaultTrait>()?.getDefaultValue(targetShape)?.let {
defaultValue(it)
if (it != "null") unboxed()
}
}
.build()
val targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) { | ||
var targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT)) { | ||
toSymbol(targetShape).toBuilder().boxed().build() | ||
} else { | ||
toSymbol(targetShape) | ||
} | ||
|
||
targetSymbol = shape.getTrait<DefaultTrait>()?.let { | ||
val builder = targetSymbol.toBuilder() | ||
val defaultValue = it.getDefaultValue(targetShape) | ||
builder.defaultValue(defaultValue) | ||
if (defaultValue != "null") { | ||
builder.unboxed() | ||
} | ||
builder.build() | ||
} ?: targetSymbol |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Looks like we box when the nullable index says to but then unbox if the default isn't null. Is that right? What happens if these two things conflict?
private fun DefaultTrait.getDefaultValue(targetShape: Shape): String = | ||
if (toNode().toString() == "null" || (targetShape is BlobShape && toNode().toString() == "")) { | ||
"null" | ||
} else if (toNode().isNumberNode) { | ||
getDefaultValueForNumber(targetShape, toNode().toString()) | ||
} else if (toNode().isArrayNode) { | ||
"listOf()" | ||
} else if (toNode().isObjectNode) { | ||
"mapOf()" | ||
} else if (toNode().isStringNode) { | ||
"\"${toNode()}\"" | ||
} else { | ||
toNode().toString() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: Prefer when
over if
/else if
/.../else
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
@@ -253,6 +284,13 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli | |||
return builder | |||
} | |||
|
|||
private fun getDefaultValueForNumber(shape: Shape, value: String) = when (shape) { | |||
is LongShape -> "${value}L" | |||
is FloatShape -> if (value.matches("[0-9]*\\.[0-9]+".toRegex())) "${value}f" else "$value.0f" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Is the decimal check necessary? The f
type specifier should mean that a decimal component is not required (i.e., 5f
and 5.0f
are identical).
/** | ||
* Mark a symbol as not being boxed | ||
*/ | ||
fun Symbol.Builder.unboxed(): Symbol.Builder = apply { removeProperty(SymbolProperty.BOXED_KEY) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Should we be keeping the notion of boxed/unboxed in our codebase at this point? It seems like the nullability index should be preferred at this point, should we rename this or change the way we represent symbol nullability?
private fun DefaultTrait.getDefaultValue(targetShape: Shape): String = | ||
if (toNode().toString() == "null" || (targetShape is BlobShape && toNode().toString() == "")) { | ||
"null" | ||
} else if (toNode().isNumberNode) { | ||
getDefaultValueForNumber(targetShape, toNode().toString()) | ||
} else if (toNode().isArrayNode) { | ||
"listOf()" | ||
} else if (toNode().isObjectNode) { | ||
"mapOf()" | ||
} else if (toNode().isStringNode) { | ||
"\"${toNode()}\"" | ||
} else { | ||
toNode().toString() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks mostly good, few minor things/questions.
toNode().isNumberNode -> getDefaultValueForNumber(targetShape, toNode().toString()) | ||
toNode().isArrayNode -> "listOf()" | ||
toNode().isObjectNode -> "mapOf()" | ||
toNode().isStringNode -> "\"${toNode()}\"" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This doesn't escape any quotes in the value. While unlikely, it's still better to be safe. Fortunately, we already have a String.dq()
extension method for exactly this:
toNode().isStringNode -> toNode().toString().dq()
val default = getProperty(SymbolProperty.DEFAULT_VALUE_KEY, String::class.java) | ||
return if (default.isPresent) default.get() else null | ||
val defaultType = default.getOrNull()?.run { getProperty(SymbolProperty.DEFAULT_VALUE_TYPE_KEY, DefaultValueType::class.java).get() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: I suggest a convenience extension property for getting the default type, similar to isNullable
.
long MyFoo | ||
""".prependNamespaceAndService().toSmithyModel() | ||
""".prependNamespaceAndService(version = "2").toSmithyModel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Should version = "2"
be the default?
if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) nullable() | ||
|
||
shape.getTrait<DefaultTrait>()?.let { | ||
defaultValue(it.getDefaultValue(targetShape), DefaultValueType.MODELED) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so if a member has @default(null)
in the model ("to explicitly indicate that the member has no default value or to override the default value requirement of a targeted shape."), this will be treated as having a MODELED default, even though semantically it makes the member nullable. So it would say nullable=true and defaultValueType=MODELED? I'm still trying to understand how MODELED is used, but just checking if this is ok/intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@default(null)
will not make the member nullable, it will just override any default value which has been potentially set by a target shape. the only thing that makes members nullable is the nullability index provided by Smithy. My other response has an explanation of INFERRED vs. MODELED
createSymbolBuilder(shape, typeName, namespace = "kotlin") | ||
.defaultValue(defaultValue) | ||
.build() | ||
createSymbolBuilder(shape, typeName, namespace = "kotlin").defaultValue(defaultValue).build() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: so all number shapes having a INFERRED default value means the shape knows what the value would be, but the shape can be nullable
too, and it's the nullable
check that'll decide how the code is generated, and if it is not nullable, only then that defaultValue is relevant? Trying to understand the motivation for maintaining INFERRED v/s MODELED.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation behind INFERRED
/ MODELED
is because we're using .defaultValue()
in two ways now. The previous way is inferring that numbers and booleans have a default value (0, 0f, 0.0, false). The new way we use .defaultValue()
is when it's modeled on a shape.
If a shape has an INFERRED
default value but it's nullable, we set it to null. If a shape has a MODELED
default value but it's nullable, we will use the default value.
val default = getProperty(SymbolProperty.DEFAULT_VALUE_KEY, String::class.java) | ||
return if (default.isPresent) default.get() else null | ||
|
||
// nullable types should default to null if there is no modeled default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably don't understand how this all fits, but why is isNullable not sufficient? Seems simpler if it ended up like that. What test case would break without the &&
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for the &&
check is if something's nullable but it has a default value modeled, we want to write that default.
With &&
check:
public val foo: String? = "my modeled default value"
Without &&
check:
public val foo: String? = null // (discards the modeled default value)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought if something has a non-null default value, it won't be nullable. Atleast when it uses CLIENT
mode, per https://github.com/awslabs/smithy/blob/b69aeb5dae6639b4381575005722bea02bb31ca8/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java#L142.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, but they can be nullable because of @clientOptional
or @input
and still have default value. got it.
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model) | ||
val member = model.expectShape<MemberShape>("com.test#MyStruct\$foo") | ||
val memberSymbol = provider.toSymbol(member) | ||
assertEquals("kotlin", memberSymbol.namespace) | ||
assertEquals("null", memberSymbol.defaultValue()) | ||
assertTrue(memberSymbol.isBoxed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems useful to assert memberSymbol.isNullable to show the behavior.
import software.amazon.smithy.kotlin.codegen.model.traits.SYNTHETIC_NAMESPACE | ||
import software.amazon.smithy.kotlin.codegen.test.* | ||
import software.amazon.smithy.model.shapes.* | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertTrue | ||
|
||
class SymbolProviderTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably worth adding cases with @clientOptional
, @addedDefault
, @input
?
And @required
on nullable target shapes? like
structure MyStruct {
@required
quux: Integer,
}
val targetSymbol = toSymbol(targetShape) | ||
.toBuilder() | ||
.apply { | ||
if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT)) nullable() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given kotlin SDK is not GA, could NullableIndex.CheckMode.CLIENT
be used?
And note, nullableIndex.isMemberNullable(shape)
uses CheckMode.CLIENT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're stuck using CLIENT_ZERO_VALUE_V1_NO_INPUT
until S3 makes all their numbers and booleans nullable. We tried changing it in this PR but faced some issues with @default
and nullability on things like PutObjectRequest's bucketKeyEnabled
.
Internal reference: P83747977
We can hold off on merging this PR until it's resolved internally, and then I can update to use CheckMode.CLIENT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it. if this needs to be merged sooner for some reason, as long as there's tracking / TODO somewhere to change to CLIENT
that sounds fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spoke with the team, we'll merge the PR now and add a tracking item to update to CLIENT
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
Adds support for the
default
trait.Issue #
#718
Description of changes
Support for the
default
trait is required for supporting Smithy IDL 2.0.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.